四、ESP32 UART如何控制LED灯?
1、流程
- 配置GPIO结构体,并使能GPIO(LED灯的引脚)
- gpio_config_t
- gpio_config(&gpio_config_t)
- 安装驱动程序 - 为 UART 驱动程序分配 ESP32-S3 资源
- 设置通信参数 - 设置波特率、数据位、停止位等
- 设置通信管脚 - 分配连接设备的管脚
- 运行 UART 通信 - 发送/接收数据
- 检测 if MSB 第1位数据是0还是1,并输出LED灯引脚高低电平,进而控制LED灯亮灭
- if (data[0] == '1')
- if (data[0] == '0')
五、什么是UART中断(FreeRTOS多任务+中断)?
5.1 先了解FreeRTOS是什么
乐鑫官方网页ESP-IDF FreeRTOS介绍:https://documentation.espressif.com/projects/esp-idf/zh_CN/latest/esp32s3/api-reference/system/freertos.html#freertos
❗ Important
因为ESP-IDF是基于C语言和FreeRTOS进行开发的,所以ESP-IDF与STM32HAL库区别很大,ESP-IDF的void app_main(void)就是FreeRTOS的主任务程序。



5.2 为什么说ESP-IDF FreeRTOS任务的有4种状态?
IDF FreeRTOS 中任务的结构与 Vanilla FreeRTOS 相同。具体而言,IDF FreeRTOS 任务:
- 只能处于以下任一状态:运行中、就绪、阻塞或挂起。
- 任务函数通常为无限循环。
- 任务函数不应返回。
5.3 ESP-IDF FreeRTOS有哪些API参考?

static inline BaseType_txTaskCreate(TaskFunction_t pxTaskCode, const char *const pcName, const configSTACK_DEPTH_TYPE usStackDepth, void *const pvParameters, UBaseType_t uxPriority, TaskHandle_t *const pxCreatedTask)
示例代码:
// Task to be created.
void vTaskCode( void * pvParameters )
{
for( ;; )
{
// Task code goes here.
}
}
// Function that creates a task.
void vOtherFunction( void )
{
static uint8_t ucParameterToPass;
TaskHandle_t xHandle = NULL;
// Create the task, storing the handle. Note that the passed parameter ucParameterToPass
// must exist for the lifetime of the task, so in this case is declared static. If it was just an
// an automatic stack variable it might no longer exist, or at least have been corrupted, by the time
// the new task attempts to access it.
xTaskCreate( vTaskCode, "NAME", STACK_SIZE, &ucParameterToPass, tskIDLE_PRIORITY, &xHandle );
configASSERT( xHandle );
// Use the handle to delete the task.
if( xHandle != NULL )
{
vTaskDelete( xHandle );
}
}5.4 如何实现UART与FreeRTOS结合的项目目标?
- 绿色的灯每间隔100毫秒(Ms)闪烁一次,要求无限循环闪烁
- 红色的LED灯每间隔1.5秒(s)闪烁一次,也同样要求无限循环闪烁
void app_main(void)
{
while(1){
gpio_set_level(GPIO_NUM_2, 1); // 开灯
vTaskDelay(pdMS_TO_TICKS(100));
gpio_set_level(GPIO_NUM_2, 0); // 关灯
vTaskDelay(pdMS_TO_TICKS(100)); // 100毫秒
gpio_set_level(GPIO_NUM_4, 1); // 开灯
vTaskDelay(pdMS_TO_TICKS(1500));
gpio_set_level(GPIO_NUM_4, 0); // 关灯
vTaskDelay(pdMS_TO_TICKS(1500)); // 1.5秒
}
}5.4.1 代码实现UART与FreeRTOS结合
#include <stdio.h>
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
void led_green_2(void *pvParameters){
for(;;){
gpio_set_level(GPIO_NUM_2, 1); // 开灯
vTaskDelay(pdMS_TO_TICKS(100));
gpio_set_level(GPIO_NUM_2, 0); // 关灯
vTaskDelay(pdMS_TO_TICKS(100)); // 100毫秒
}
}
void led_red_4(void * pvParameters){
for(;;){
gpio_set_level(GPIO_NUM_4, 1); // 开灯
vTaskDelay(pdMS_TO_TICKS(1500));
gpio_set_level(GPIO_NUM_4, 0); // 关灯
vTaskDelay(pdMS_TO_TICKS(1500)); // 1.5秒
}
}
void app_main(void)
{
// 一、配置GPIO结构,再使能GPIO
gpio_config_t my_gpio_config = {
.pin_bit_mask = (1ULL << GPIO_NUM_2) | (1ULL << GPIO_NUM_4),
.mode = GPIO_MODE_OUTPUT,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.pull_up_en = GPIO_PULLUP_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&my_gpio_config);
// 二、创建FreeRTOS task,封装task任务函数,最后传参到xTaskCreate()
xTaskCreate(led_green_2, "led_green_2", 2048, NULL, 0, NULL);
xTaskCreate(led_red_4, "led_red_4", 1024, NULL, 0, NULL);
}
六、什么是UART中断(Queue队列+非阻塞等待任务触发)?
6.0 如何理解中断概念?
- 中断的概念
- 中断优先级的概念
- 中断处理机制






6.1 ESP-IDF框架下ESP32-S3中断与STM32中断比对
1. 不同单片机的中断处理流程对比:STM32 vs ESP32 对比图
1.1 STM32(裸机):
中断 → 直接进 ISR → 点亮 LED
✅ 简单直接
1.2 ESP32(FreeRTOS):
中断 → ISR → ??? → 点亮 LED
❌ ISR 里不能做复杂事!
2. 解释说明:
“在 ESP32 上,ISR(中断服务函数)运行在高危环境:
2.1 不能调用大多数 API(比如 printf、malloc)
2.2 不能访问 Flash(可能卡死)
2.3 必须快进快出(微秒级)
3. 所以,这就是为什么ESP-IDF官方禁止在 ISR 里直接处理业务逻辑
那怎么办?答案是:让 ISR 只发一个‘通知’,真正的处理交给后台任务!”
3.1 这里的ISR(中断服务函数)就类似于战争时期指挥部的通信兵,
3.2 首先侦察兵接收到前方突发敌袭、炮轰(也就是中断源触发了),
3.3 然后经过中断矩阵(侦察兵经过营地找通信兵),
3.4 通信兵接收到消息后应该要立刻将敌袭消息发送到队列,任务获取Queue队列的消息,执行任务
3.5 ISR接下来继续等待中断源通道(侦察兵)再次来消息即可
🚨 严重错误!
3.4.1 ISR必须响应极快,而不是先去拉泡屎、撒泡尿、吃个饭、撩下妹再来发送敌袭消息,延误了战机要拉出去执行军令状的。


6.2 中断前置知识(FreeRTO之Queue队列+多任务)
💡 Tip
- 队列可以跨任务通信传递消息
| 章节编号 | 章节名称 | 核心内容(小白友好版) | 时长占比 | 官方文档对标 |
|---|---|---|---|---|
| 0 | 课前回顾 & 预告 | 1. 回顾已讲:UART 轮询控制 LED(官方 1-4 步); 2. 抛出痛点:轮询延迟高、占 CPU; 3. 预告:用中断 + FreeRTOS 解决,今天学 2 个核心:队列 / 任务 + UART 中断 | 2 分钟 | 无(衔接内容) |
| 1 | 前置知识:FreeRTOS 队列 & 任务(仅讲和 UART 中断相关的) | 1. 队列(Queue): - 比喻:前台(中断)不能处理复杂工作,把消息丢进 “传达室信箱(队列)”,后台(任务)慢慢取; - 核心 API:xQueueReceive(取消息); - 关键参数:阻塞时间(portMAX_DELAY= 等不到消息不干活); 2. 任务(Task): - 比喻:专门 “处理信箱消息” 的后台员工; - 核心 API:xTaskCreate(雇一个后台员工); - 关键参数:优先级(员工优先级高于前台); 3. 中断 + 队列 + 任务的关系:画流程图(中断→队列→任务) | 5 分钟 | FreeRTOS 官方文档 |
| 2 | 官方流程第 5 步:UART 中断(核心) | 按官方流程拆解,只讲 “使用中断” 的关键步骤: 1. 步骤 1:修改uart_driver_install(创建队列,官方第一步); 2. 步骤 2:配置 UART 中断阈值(1 字节触发); 3. 步骤 3:启用 UART 接收中断; 4. 步骤 4:创建事件处理任务(读取队列 + 控制 LED) | 8 分钟 | UART 中断官方文档 |
| 3 | 代码实战 & 效果演示 | 1. 逐行讲解修改后的代码(标注新增 / 修改); 2. 烧录测试: - 发 1→LED 立即亮(无 500ms 延迟); - 发 0→LED 立即灭; - 快速发 1010→LED 快速闪烁(对比轮询的卡顿); 3. 打印日志:主任务空闲(证明不占 CPU) | 4 分钟 | 无(实战验证) |
| 4 | 避坑指南 & 答疑 | 1. 常见坑: - 坑 1:队列没创建(uart_driver_install队列参数错)→ 解决:检查第 6 个参数; - 坑 2:中断阈值设太大→ 解决:设为 1; - 坑 3:任务栈太小→ 解决:设 4096; 2. 答疑:为什么中断标志位设 0?(官方默认配置) | 2 分钟 | UART 驱动安装 API 文档 |
| 5 | 总结 & 拓展 | 1. 核心总结:3 个关键点(队列传消息、任务处理、1 字节触发中断); 2. 拓展:下节课讲模式检测中断(识别 +++) | 1 分钟 | 无 |

6.3 ESP-IDF的UART中断事件处理流程是怎么样的?
你配置 uart_intr_config_t
↓
UART 硬件在满足条件时产生中断(如 RX 超时)
↓
ESP-IDF 的 UART ISR 被调用
↓
ISR 将硬件中断“翻译”成 uart_event_type_t(如 UART_DATA)
↓
事件被放入你创建的 uart_event_queue
↓
你的任务通过 xQueueReceive() 读取 uart_event_t 并处理

6.4 中断ESP-IDF官方示例代码讲了什么?
const uart_port_t uart_num = UART_NUM_2;
// Configure a UART interrupt threshold and timeout
uart_intr_config_t uart_intr = {
.intr_enable_mask = UART_INTR_RXFIFO_FULL | UART_INTR_RXFIFO_TOUT,
.rxfifo_full_thresh = 100,
.rx_timeout_thresh = 10,
};
ESP_ERROR_CHECK(uart_intr_config(uart_num, &uart_intr));
// Enable UART RX FIFO full threshold and timeout interrupts
ESP_ERROR_CHECK(uart_enable_rx_intr(uart_num));
6.5 UART中断流程代码步骤如何编写?
- 创建Queue队列句柄,方便后期使用队列句柄传送中断消息给任务函数(中断 + 队列 + 任务的关系:(中断→队列→任务))
- 配置GPIO结构体,并使能GPIO(2颗LED灯)
- 配置UART及UART中断
- 安装驱动程序 - 为 UART 驱动程序分配 ESP32-S3 资源
- 步骤 1:修改uart_driver_install(创建队列,官方第一步);
- 设置通信参数 - 设置波特率、数据位、停止位等
- 设置通信管脚 - 分配连接设备的管脚
- 运行 UART 通信 - 发送/接收数据
- 使用中断 - 触发特定通信事件的中断
- 步骤 2:配置 UART 中断阈值(1 字节触发);
- 步骤 3:启用 UART 接收中断;
- 安装驱动程序 - 为 UART 驱动程序分配 ESP32-S3 资源
- 创建事件处理任务(读取队列 + 控制 LED)
- 创建FreeRTO Task任务,
- 创建Task任务的同时,编写并封装好中断服务函数
- Task任务①为GPIO2号绿色LED灯每隔100毫秒闪烁一次
- Task任务②为GPIO4号红色LED灯被UART中断控制(中断触发→队列传送中断被触发的消息→Task任务②唤醒并执行任务)

6.6 代码实现UART中断(Queue队列+非阻塞等待任务触发)
#include <stdio.h>
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"
#include "hal/uart_hal.h"
#include "esp_log.h"
// 新增:队列句柄(中断事件的“中转站”)
QueueHandle_t my_uart_intr_queue_handle;
void my_led_initialize(void){
gpio_config_t my_gpio_config = {
.pin_bit_mask = (1ULL << GPIO_NUM_2) | (1ULL << GPIO_NUM_4),
.mode = GPIO_MODE_OUTPUT,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.pull_up_en = GPIO_PULLUP_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&my_gpio_config);
gpio_set_level(GPIO_NUM_4, 0);
}
void led_green_2(void *pvParameters){
for(;;){
gpio_set_level(GPIO_NUM_2, 1); // 开灯
vTaskDelay(pdMS_TO_TICKS(100));
gpio_set_level(GPIO_NUM_2, 0); // 关灯
vTaskDelay(pdMS_TO_TICKS(100)); // 100毫秒
}
}
void led_red_4(void * pvParameters)
{ uart_event_t my_uart_intr_event_type;
uint8_t rx_buffer_zone[128];
for(;;)
{
if(xQueueReceive(my_uart_intr_queue_handle, &my_uart_intr_event_type, portMAX_DELAY)){
if( my_uart_intr_event_type.type == UART_DATA && my_uart_intr_event_type.size > 0){
ESP_LOGI("uart_intr_trigger", "bytes length: %d", my_uart_intr_event_type.size);
uart_read_bytes(UART_NUM_0, rx_buffer_zone, my_uart_intr_event_type.size, pdMS_TO_TICKS(100));
if (my_uart_intr_event_type.size >= 6 && memcmp(rx_buffer_zone, "led_on", 6) == 0 ){
gpio_set_level(GPIO_NUM_4, 1);
ESP_LOGI("Received CMD", "LED ON!");
}
if (my_uart_intr_event_type.size >= 7 && memcmp(rx_buffer_zone, "led_off", 7) == 0 ){
gpio_set_level(GPIO_NUM_4, 0);
ESP_LOGI("Received CMD", "LED OFF!");
}
}
}
}
}
void app_main(void)
{ /*一、配置GPIO结构,再使能GPIO*/
my_led_initialize();
/*二、注册UART及启动中断,配置UART结构体parameters并使能,配置UART RX以及TX引脚*/
uart_config_t my_uart_tele_config = {
.baud_rate = 115200,
.data_bits = UART_DATA_8_BITS,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.source_clk = UART_SCLK_DEFAULT,
};
uart_driver_install(UART_NUM_0, 1024, 0, 2, &my_uart_intr_queue_handle, 0);
uart_param_config(UART_NUM_0, &my_uart_tele_config);
uart_set_pin(UART_NUM_0, GPIO_NUM_43, GPIO_NUM_44, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
/*三、显示配置UART中断参数并启用uart intr config() */
uart_intr_config_t my_uart_intr_config = {
.intr_enable_mask = UART_INTR_RXFIFO_FULL | UART_INTR_RXFIFO_TOUT,
.rx_timeout_thresh = 10,
.rxfifo_full_thresh = 64,
.txfifo_empty_intr_thresh = 0,
};
ESP_ERROR_CHECK( uart_intr_config(UART_NUM_0, &my_uart_intr_config) );
/* 使能rx环形缓冲区中断 */
ESP_ERROR_CHECK( uart_enable_rx_intr(UART_NUM_0) );
/*四、创建FreeRTOS task,封装task任务函数,最后传参到xTaskCreate()*/
xTaskCreate(led_green_2, "led_green_2", 1024, NULL, 0, NULL);
xTaskCreate(led_red_4, "led_red_4", 2048, NULL, 0, NULL);
}#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"
#include "driver/gpio.h"
#include "string.h"
#include "esp_log.h"
// 你UART的引脚定义
#define tx_pin GPIO_NUM_43
#define rx_pin GPIO_NUM_44
// 新增:日志标签(方便调试)
#define TAG "UART_INTERRUPT"
// 新增:队列句柄(中断事件的“中转站”)
QueueHandle_t uart_event_queue;
// LED GPIO初始化
void led_init(void) {
gpio_config_t my_led_gpio_config = {0};
my_led_gpio_config.pin_bit_mask = 1ULL << GPIO_NUM_2;
my_led_gpio_config.mode = GPIO_MODE_OUTPUT;
my_led_gpio_config.pull_down_en = GPIO_PULLDOWN_DISABLE;
my_led_gpio_config.pull_up_en = GPIO_PULLUP_DISABLE;
my_led_gpio_config.intr_type = GPIO_INTR_DISABLE;
gpio_config(&my_led_gpio_config);
// 新增:初始熄灭LED
gpio_set_level(GPIO_NUM_2, 0);
}
// 新增:UART事件处理任务(专门处理中断事件,FreeRTOS核心)
void uart_event_task(void *arg) {
uart_event_t event; // UART事件结构体(SDK定义)
size_t buffered_len;
uint8_t data[128] = {0};
// 死循环:一直等待队列中的中断事件(理解:“一直看短信”)
for(;;) {
// 从队列读取事件:portMAX_DELAY=永久阻塞,直到有事件
if (xQueueReceive(uart_event_queue, &event, portMAX_DELAY)) {
switch (event.type) {
// 事件1:收到UART数据(中断触发)
case UART_DATA:
// 获取缓冲区中的数据长度(替代你原来的uart_get_buffered_data_len)
uart_get_buffered_data_len(UART_NUM_0, &buffered_len);
// 读取数据,UART核心控制器读取接收数据缓冲区数据
int rxBytes = uart_read_bytes(UART_NUM_0, data, buffered_len, 100);
if (rxBytes > 0) {
data[rxBytes] = 0; // 字符串结束符
ESP_LOGI(TAG, "收到数据:%s", data);
// 原来的LED控制逻辑
if (data[0] == '1') {
gpio_set_level(GPIO_NUM_2, 1);
}
if (data[0] == '0') {
gpio_set_level(GPIO_NUM_2, 0);
}
}
break;
// 事件2:FIFO溢出(避坑:数据太多没及时处理)
case UART_FIFO_OVF:
ESP_LOGE(TAG, "警告:UART FIFO溢出!");
uart_flush(UART_NUM_0); // 清空缓冲区
break;
// 其他事件(可暂时忽略,留拓展空间)
default:
ESP_LOGW(TAG, "未处理的UART事件:%d", event.type);
break;
}
}
}
// 任务删除(实际不会执行到)
vTaskDelete(NULL);
}
void app_main(void) {
// 保留:LED初始化
led_init();
// UART配置结构体(仅新增注释,内容不变)
uart_config_t my_uart_config = {0};
my_uart_config.baud_rate = 115200;
my_uart_config.data_bits = UART_DATA_8_BITS;
my_uart_config.parity = UART_PARITY_DISABLE;
my_uart_config.stop_bits = UART_STOP_BITS_1;
my_uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
my_uart_config.source_clk = UART_SCLK_DEFAULT;
// 修改:uart_driver_install(新增队列参数,核心改动!)
// 原代码:uart_driver_install(UART_NUM_0, 1024, 0, 0, NULL, 0);
// 新代码:最后4个参数:队列深度=5,队列句柄=uart_event_queue,中断标志=0
uart_driver_install(UART_NUM_0, 1024, 0, 5, &uart_event_queue, 0);
// UART参数配置
uart_param_config(UART_NUM_0, &my_uart_config);
// UART引脚配置
uart_set_pin(UART_NUM_0, tx_pin, rx_pin, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
// 新增:UART中断配置(重点:设置1字节触发中断)
uart_intr_config_t uart_intr = {0};
uart_intr.intr_enable_mask = UART_INTR_RXFIFO_FULL | UART_INTR_RXFIFO_TOUT;
uart_intr.rxfifo_full_thresh = 1; // 收到1个字节就触发中断(无延迟)
uart_intr.rx_timeout_thresh = 10; // 超时阈值(防止漏数据)
uart_intr_config(UART_NUM_0, &uart_intr);
// 新增:启用UART接收中断(SDK自动处理底层中断矩阵)
uart_enable_rx_intr(UART_NUM_0);
// 新增:创建FreeRTOS任务(处理UART事件)
// 参数:任务函数、任务名、栈大小、参数、优先级、任务句柄
xTaskCreate(
uart_event_task, // 任务函数(上面定义的)
"uart_event_task", // 任务名(给任务起个名字)
4096, // 栈大小(4096足够,不用改)
NULL, // 任务参数(无)
10, // 优先级(高于主任务的默认优先级1)
NULL // 任务句柄(不用保存)
);
// 修改:主循环(去掉轮询逻辑,改为“闲任务”)
// 原代码:while(1)轮询读取UART数据
// 新代码:主任务只打印日志,证明不占CPU
while (1) {
ESP_LOGI(TAG, "主任务运行中(CPU空闲)...");
vTaskDelay(pdMS_TO_TICKS(1000)); // 1秒打印一次
}
}